#实战 Kaggle 比赛：预测房价
#实现几个函数来方便下载数据
import hashlib
import os
import tarfile
import zipfile
import requests


DATA_HUB = dict()
DATA_URL = 'http://d2l-data.s3-accelerate.amazonaws.com/'

def download(name, cache_dir=os.path.join('..', 'data')):
    """下载一个DATA_HUB中的文件，返回本地文件名。"""
    assert name in DATA_HUB, f"{name} 不存在于 {DATA_HUB}."
    url, sha1_hash = DATA_HUB[name]
    os.makedirs(cache_dir, exist_ok=True)
    fname = os.path.join(cache_dir, url.split('/')[-1])
    if os.path.exists(fname):
        sha1 = hashlib.sha1()
        with open(fname, 'rb') as f:
            while True:
                data = f.read(1048576)
                if not data:
                    break
                sha1.update(data)
        if sha1.hexdigest() == sha1_hash:
            return fname
    print(f'正在从{url}下载{fname}...')


#使用pandas读入并处理数据
import numpy as np
import pandas as pd
import torch
from torch import nn
from d2l import torch as d2l

DATA_HUB['kaggle_house_train'] = (
    DATA_URL + 'kaggle_house_pred_train.csv',
    '585e9cc93e70b39160e7921475f9bcd7d31219ce')

DATA_HUB['kaggle_house_test'] = (
    DATA_URL + 'kaggle_house_pred_test.csv',
    'fa19780a7b011d9b009e8bff8e99922a8ee2eb90')

train_data = pd.read_csv(download('kaggle_house_train'))
test_data = pd.read_csv(download('kaggle_house_test'))

print(train_data.shape)
print(test_data.shape)

#前四个和最后两个特征，以及相应标签
print(train_data.iloc[0:4, [0, 1, 2, 3, -3, -2, -1]])
#前面的冒号就是取行数，后面的冒号是取列数
#在每个样本中，第一个特征是ID， 我们将其从数据集中删除
all_features = pd.concat((train_data.iloc[:, 1:-1], test_data.iloc[:, 1:]))#https://www.cnblogs.com/shueixue/p/10953699.html
#numpy提供了numpy.concatenate((a1,a2,...), axis=0)函数。能够一次完成多个数组的拼接。其中a1,a2,...是数组类型的参数
#iloc[]函数，属于pandas库，全称为index location，即对数据进行位置索引，从而在数据表中提取出相应的数据
#https://blog.csdn.net/Fwuyi/article/details/123127754
#将所有缺失的值替换为相应特征的平均值。 通过将特征重新缩放到零均值和单位方差来标准化数据
# object是str类或数字混合类型（mixed），将特征为数字的列单独拿出。并保存列名到numeric
numeric_features = all_features.dtypes[all_features.dtypes != 'object'].index
#https://www.cnblogs.com/hahaah/p/15067930.html
#all_features.dtypes：返回每个列数值类型
#all_features.dtypes !='object'：返回每个列的布尔值，类型不满足object为True否则为False。
#all_features.dtypes[all_features.dtypes !='object']：将类型为object的列去除。
#all_features.dtypes[all_features.dtypes !='object'].index：返回类型不为object列的列名。
#Python中的str和Numpy中的string、unicode（字符编码） ，在Pandas中都表示为object，也就是字符串在Pandas中的类型为object。
# 若⽆法获得测试数据，则可根据训练数据计算均值和标准差
all_features[numeric_features] = all_features[numeric_features].apply(
    lambda x: (x - x.mean()) / (x.std()))#https://blog.csdn.net/yanjiangdi/article/details/94764562
#.std计算标准差
# 对每种数字型的特征(每一的数字)进行标准化
all_features[numeric_features] = all_features[numeric_features].fillna(0)
#标准化后，每个数值特征的均值变为0，所以可以直接用0代替缺失值,即缺失值NA和无意义值NAN用0代替
#处理离散值。 我们用一次独热编码替换它们
all_features = pd.get_dummies(all_features, dummy_na=True)


#从pandas格式中提取NumPy格式，并将其转换为张量表示
#取train_data的行数，即训练集个数
n_train = train_data.shape[0]
# 训练集特征
train_features = torch.tensor(all_features[:n_train].values,
                              dtype=torch.float32)
# 测试集特征
test_features = torch.tensor(all_features[n_train:].values,
                         dtype=torch.float32)
# 训练标签 将shape=(1460)转换为shape=(1460,1)
train_labels = torch.tensor(train_data.SalePrice.values.reshape(-1, 1),
                            dtype=torch.float32)

#训练
loss = nn.MSELoss()
in_features = train_features.shape[1]
print('******************************')
print(in_features)
print(train_features)

def get_net():
    net = nn.Sequential(nn.Linear(in_features, 1))
    return net

#我们更关心相对误差(y-y^)/y， 解决这个问题的一种方法是用价格预测的对数来衡量差异
#将输入input张量每个元素的范围限制到区间 [min,max]，返回结果到一个新张量。
def log_rmse(net, features, labels):
    # 为了在取对数时进一步稳定该值，将小于1的值设置为1
    clipped_preds = torch.clamp(net(features), 1, float('inf'))
    #float('inf')表示正无穷
    # torch.clamp(input, min, max, out=None)将输入input张量每个元素的范围限制到区间 [min,max]，返回结果到一个新张量。
    rmse = torch.sqrt(loss(torch.log(clipped_preds), torch.log(labels)))
    return rmse.item()#dict.items()	以列表返回可遍历的（键，值）元组数组

#我们的训练函数将借助Adam优化器
#Adam对学习率没有那么那么敏感
def train(net, train_features, train_labels, test_features, test_labels,
          num_epochs, learning_rate, weight_decay, batch_size):
    train_ls, test_ls = [], []
    train_iter = d2l.load_array((train_features, train_labels), batch_size)
    optimizer = torch.optim.Adam(net.parameters(), lr=learning_rate,
                                 weight_decay=weight_decay)#weight_decay权重衰退
    for epoch in range(num_epochs):
        for X, y in train_iter:
            optimizer.zero_grad()
            l = loss(net(X), y)
            l.backward()
            optimizer.step()
        train_ls.append(log_rmse(net, train_features, train_labels))#损失放入列表
        if test_labels is not None:
            test_ls.append(log_rmse(net, test_features, test_labels))#损失放入列表
    return train_ls, test_ls

#K折交叉验证
def get_k_fold_data(k, i, X, y):
    assert k > 1
    fold_size = X.shape[0] // k
    X_train, y_train = None, None
    for j in range(k):
        # slice(start, stop[, step])；start:开始位置，stop:结束位置，step：间距
        # 则idx等于就是第j个集合切片对象的集合
        idx = slice(j * fold_size, (j + 1) * fold_size)
        #slice() 函数返回 slice 对象（切片）。
        #slice 对象用于指定如何对序列进行裁切。
        # 您可以指定在哪里开始裁切以及在哪里结束裁切。您还可以指定步进，例如只切每隔一个项目。
        ## 将第j个集合的特征，和第j个集合的标签分别放在X_part, y_part
        X_part, y_part = X[idx, :], y[idx]
        # 如果当前的集合是第i折交叉验证，就将当前的集合当作验证模型
        # （j=i）就是当前所取部分刚好是要当验证集的部分
        if j == i:
            X_valid, y_valid = X_part, y_part
        # 如果j!=i且X_train是空的则直接将此部分放进训练集
        elif X_train is None:
            X_train, y_train = X_part, y_part
        #如果j!=i且访问到其余除了验证集（j=i）其余集合的子集，就使用concat连接已经放进训练集的集合
        else:
            X_train = torch.cat([X_train, X_part], 0)
            y_train = torch.cat([y_train, y_part], 0)
            #torch.cat ( (A, B), dim=0)接受一个由两个（或多个）tensor组成的元组，按行拼接，所以两个（多个）tensor的列数要相同。
            #torch.cat ( (A, B), dim=1)是按列拼接，所以两个tensor的行数要相同。
    # 依次返回训练数据和第i个验证数据
    return X_train, y_train, X_valid, y_valid

#返回训练和验证误差的平均值
def k_fold(k, X_train, y_train, num_epochs, learning_rate, weight_decay,
           batch_size):
    ##初始化训练集损和验证集的损失和
    train_l_sum, valid_l_sum = 0, 0
    # 依次访问所划分集合
    for i in range(k):
        # 获得训练模型和第i个验证模型
        data = get_k_fold_data(k, i, X_train, y_train)
        # 获得net实例
        net = get_net()
        # 分别计算出训练集与验证集损失
        # *data可以分别读取数据，在这里等于是得到了get_k_fold_data)函数的return的四个表，直接写data会导致缺
        train_ls, valid_ls = train(net, *data, num_epochs, learning_rate,
                                   weight_decay, batch_size)
        # 计算训练集损失之和
        train_l_sum += train_ls[-1]
        # 计算验证集损失之和
        valid_l_sum += valid_ls[-1]
        # 画图
        if i == 0:
            # d21:自己封装的包，包里是常用的函数模块
            d2l.plot(list(range(1, num_epochs + 1)), [train_ls, valid_ls],
                     xlabel='epoch', ylabel='rmse', xlim=[1, num_epochs],
                     legend=['train', 'valid'], yscale='log')
            d2l.plt.show()
        print(f'fold {i + 1}, train log rmse {float(train_ls[-1]):f}, '
              f'valid log rmse {float(valid_ls[-1]):f}')
    return train_l_sum / k, valid_l_sum / k

#模型选择
#我们使用一组未经调优的超参数并计算交叉验证误差，可以改动这些超参数来尽可能减小平均测试误差
k, num_epochs, lr, weight_decay, batch_size = 5, 100, 5, 0, 64
train_l, valid_l = k_fold(k, train_features, train_labels, num_epochs, lr,
                          weight_decay, batch_size)
print(f'{k}-折验证: 平均训练log rmse: {float(train_l):f}, '
      f'平均验证log rmse: {float(valid_l):f}')

#提交你的Kaggle预测
#下面定义的预测函数。在预测之前，我们会使用完整的训练数据集来重新训练模型，转换预测结果存成提交所需要的格式。
def train_and_pred(train_features, test_feature, train_labels, test_data,
                   num_epochs, lr, weight_decay, batch_size):
    # 获得net实例
    net = get_net()
    # 返回训练集损失
    # 单下划线 _ 单个独立下划线是用作一个名字，来表示某个变量是临时的或无关紧要的。
    train_ls, _ = train(net, train_features, train_labels, None, None,
                        num_epochs, lr, weight_decay, batch_size)
    # 作图
    d2l.plot(np.arange(1, num_epochs + 1), [train_ls], xlabel='epoch',
             ylabel='log rmse', xlim=[1, num_epochs], yscale='log')
    d2l.plt.show()

    print(f'train log rmse {float(train_ls[-1]):f}')
    # 计算预测标签
    # detach:返回一个新的tensor,从当前图形分离（官方文档解释）
    preds = net(test_features).detach().numpy()
    #SalePrice标签列
    #reshape(1,-1)转化成一行
    #Series是一个pandas的一维数组
    # （1459，1）变成（1459，）
    test_data['SalePrice'] = pd.Series(preds.reshape(1, -1)[0])#https://blog.csdn.net/TeFuirnever/article/details/94331545
    # 将测试集的Id和预测结果拼接在一起，axis沿水平方向拼接，与上面默认纵向拼接不同。
    submission = pd.concat([test_data['Id'], test_data['SalePrice']], axis=1)
    # 转成可提交的csv格式
    submission.to_csv('submission.csv', index=False)
#_设计好模型并调好超参数之后，接下来就是对测试数据集上的房屋样本做价格预测。如果我们得到与交叉验证时差不多的训练误差，
# 那么这个结果很可能是理想的，可以在Kaggle上提交结果
train_and_pred(train_features, test_features, train_labels, test_data,
               num_epochs, lr, weight_decay, batch_size)